<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <title>播放端</title>
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <style>
    :root{--bg:#fafafa;--card:#fff;--muted:#888;--border:#e6e6e6}
    body{font-family:system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial;padding:16px;background:var(--bg);color:#222}
    h3{margin:0 0 12px}
    .controls{display:flex;gap:8px;align-items:center;margin-bottom:10px}
    canvas{background:#000;max-width:100%;border-radius:6px;display:block}
    .log-panel{margin-top:12px;border-radius:8px;border:1px solid var(--border);background:var(--card);overflow:hidden}
    .log-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid #eee}
    .log-actions{display:flex;gap:8px;align-items:center}
    table.log-table{width:100%;border-collapse:collapse;font-size:13px}
    table.log-table thead th{background:transparent;padding:8px 10px;border-bottom:1px solid #eee;text-align:left;font-weight:700}
    table.log-table tbody td{padding:8px 10px;border-bottom:1px solid #f3f3f3;vertical-align:middle}
    .ksize{min-width:72px;text-align:left;color:#666;font-size:12px}
    .timestamp{color:var(--muted);font-size:12px;min-width:78px}
    .log-body{max-height:300px;overflow:auto}
    button{padding:6px 12px;border-radius:6px;border:1px solid #ccc;background:#fff;cursor:pointer}
    .status { font-weight:600; margin-left:8px }
    .stats { margin-left:12px; color:var(--muted); font-size:13px }
    .controls .spacer { flex:1; }
    .small { font-size:13px; color:var(--muted) }
  </style>
</head>
<body>
  <h3>播放端</h3>

  <div class="controls">
    <div id="status" class="status">状态: 未连接</div>
    <div id="stats" class="stats">帧: 0 | 带宽: 0 KB/s</div>
    <div class="spacer"></div>
  </div>

  <canvas id="canvas"></canvas>

  <div class="log-panel" style="margin-top:14px">
    <div class="log-header">
      <div><strong>日志</strong> <small class="small" style="margin-left:8px">（最近 500 条，已批量刷新）</small></div>
      <div class="log-actions">
        <button id="clearLogBtn">清空日志</button>
        <label class="small" style="display:flex;align-items:center;gap:6px">
          <input id="filterRecv" type="checkbox" checked> 显示接收帧日志
        </label>
      </div>
    </div>
    <div class="log-body">
      <table class="log-table" id="logTable" aria-label="日志表">
        <thead>
          <tr><th class="timestamp">时间</th><th style="width:86px">类型</th><th>消息</th><th class="ksize">大小</th></tr>
        </thead>
        <tbody id="logTbody"></tbody>
      </table>
    </div>
  </div>

  <script>
    (function(){
      // ---------- 配置 ----------
      const MAX_LOG_ENTRIES = 500;
      const LOG_FLUSH_MS = 150; // 批量刷新频率（ms）
      const FRAME_SUMMARY_INTERVAL = 500; // 对高频帧进行汇总并写入日志的间隔（ms）

      // ---------- DOM ----------
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      const statusEl = document.getElementById('status');
      const statsEl = document.getElementById('stats');

      const logTbody = document.getElementById('logTbody');
      const clearLogBtn = document.getElementById('clearLogBtn');
      const filterRecv = document.getElementById('filterRecv');

      // ---------- LogManager（批量更新、合并重复消息） ----------
      class LogManager {
        constructor(tbody, opts={}) {
          this.tbody = tbody;
          this.max = opts.max || MAX_LOG_ENTRIES;
          this.queue = [];
          this.flushTimer = setInterval(()=>this.flush(), LOG_FLUSH_MS);
        }
        add(type, message, bytes=null, meta={}) {
          // 按需过滤：如果是接收帧日志且用户关闭，则直接丢弃小额记录（但仍维护统计）
          if (type === 'RECV' && !filterRecv.checked && !meta.force) return;
          const now = new Date();
          this.queue.push({type, message, bytes, ts: now, meta});
        }
        flush() {
          if (this.queue.length === 0) return;
          const frag = document.createDocumentFragment();
          const toAppend = this.queue.splice(0, this.queue.length);
          for (let rec of toAppend) {
            const tr = document.createElement('tr');
            const tsTd = document.createElement('td');
            tsTd.className = 'timestamp';
            tsTd.textContent = rec.ts.toLocaleTimeString();

            const typeTd = document.createElement('td');
            typeTd.className = 'type-' + rec.type;
            typeTd.textContent = rec.type;

            const msgTd = document.createElement('td');
            msgTd.className = 'msg';
            msgTd.title = rec.message;
            msgTd.textContent = rec.message;

            const sizeTd = document.createElement('td');
            sizeTd.className = 'ksize';
            sizeTd.textContent = (typeof rec.bytes === 'number' ? formatKB(rec.bytes) : '');

            tr.appendChild(tsTd);
            tr.appendChild(typeTd);
            tr.appendChild(msgTd);
            tr.appendChild(sizeTd);
            frag.appendChild(tr);
          }
          this.tbody.appendChild(frag);
          while (this.tbody.children.length > this.max) {
            this.tbody.removeChild(this.tbody.firstChild);
          }
          // 减少控制台输出量，使用 debug
          console.debug('Flushed', toAppend.length, 'logs');
        }
        clear() {
          this.queue = [];
          this.tbody.innerHTML = '';
        }
        destroy() {
          clearInterval(this.flushTimer);
        }
      }

      function formatKB(bytes) {
        if (typeof bytes !== 'number' || isNaN(bytes)) return '';
        if (bytes >= 1024*1024) return (bytes / (1024*1024)).toFixed(2) + ' MB';
        return (bytes / 1024).toFixed(1) + ' KB';
      }

      const logManager = new LogManager(logTbody);

      clearLogBtn.onclick = () => logManager.clear();

      // ---------- WebSocket 与帧处理（接收端） ----------
      const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
      const url = scheme + '://' + location.host + '/video/viewer';
      const ws = new WebSocket(url);
      ws.binaryType = 'arraybuffer';

      // 帧统计（按秒统计）
      let recvCount = 0, recvBytes = 0, lastSummaryTs = performance.now();
      let lastSecondCount = 0, lastSecondBytes = 0;

      // 当收到二进制帧时，只做必要操作并维护统计；每隔 FRAME_SUMMARY_INTERVAL 写入一条汇总日志
      ws.onopen = () => {
        statusEl.textContent = '状态: 已连接';
        logManager.add('INFO', 'WebSocket 已连接到 /video/viewer', null, {force:true});
      };
      ws.onclose = () => {
        statusEl.textContent = '状态: 已关闭';
        logManager.add('INFO', 'WebSocket 已关闭', null, {force:true});
      };
      ws.onerror = (e) => {
        statusEl.textContent = '状态: 错误';
        logManager.add('ERROR', 'WebSocket 错误: ' + (e && e.message ? e.message : JSON.stringify(e)), null, {force:true});
      };

      // helper: 定期刷新统计显示（每 500ms）
      setInterval(()=>{
        const now = performance.now();
        const elapsed = Math.max(0.001, (now - lastSummaryTs) / 1000);
        const kbps = (lastSecondBytes / elapsed) / 1024;
        statsEl.textContent = `帧: ${lastSecondCount} | 带宽: ${kbps.toFixed(1)} KB/s`;
        // reset per-second counters
        lastSecondCount = 0; lastSecondBytes = 0; lastSummaryTs = now;
      }, 1000);

      // onmessage
      ws.onmessage = async (ev) => {
        try {
          if (ev.data instanceof ArrayBuffer) {
            const arr = ev.data;
            const byteLength = arr.byteLength || 0;

            // 更新统计（高频）
            recvCount++;
            recvBytes += byteLength;
            lastSecondCount++;
            lastSecondBytes += byteLength;

            // 每帧绘制到 canvas（使用 createImageBitmap 更高效）
            const blob = new Blob([arr], {type: 'image/jpeg'});
            if ('createImageBitmap' in window) {
              // 不等待日志：绘制和日志分离，保证渲染优先
              createImageBitmap(blob).then(bitmap => {
                canvas.width = bitmap.width;
                canvas.height = bitmap.height;
                ctx.drawImage(bitmap, 0, 0);
                if (bitmap.close) bitmap.close();
              }).catch(err=>{
                logManager.add('ERROR','createImageBitmap 绘制失败: '+ err, null);
              });
            } else {
              const img = new Image();
              img.onload = () => {
                canvas.width = img.naturalWidth;
                canvas.height = img.naturalHeight;
                ctx.drawImage(img, 0, 0);
                URL.revokeObjectURL(img.src);
              };
              img.src = URL.createObjectURL(blob);
            }

            // 每隔一定间隔写一条汇总日志（避免每帧都写）
            const now = performance.now();
            if (!this._lastFrameSummaryAt) this._lastFrameSummaryAt = 0;
            if (now - this._lastFrameSummaryAt > FRAME_SUMMARY_INTERVAL) {
              this._lastFrameSummaryAt = now;
              logManager.add('RECV', `接收帧: count=${recvCount} bytes=${formatKB(byteLength)}`, byteLength);
              // 重置 recvCount（只是用于日志信息），但不重置 lastSecond*
              recvCount = 0; recvBytes = 0;
            }
          } else {
            logManager.add('INFO', `[WS 文本] ${ev.data}`, null, {force:true});
          }
        } catch (e) {
          logManager.add('ERROR', '处理接收帧时出错: ' + e, null, {force:true});
        }
      };

      // 清理 on unload
      window.addEventListener('beforeunload', ()=>{ ws.close(); logManager.destroy(); });

    })();
  </script>
</body>
</html>
